#define _CRT_SECURE_NO_WARNINGS
#include <stdlib.h> // for atoi,malloc
#include <memory.h> // for memset
#include <stdio.h> // for FILE, printf
#include "opus/opus.h"

int main(const int argc, const char** const argv) {
	const opus_uint32 FRAME_RATE = 48000;
	const opus_int32  FRAMES_SKIP = FRAME_RATE * 13 / 2000; // 312 (*6.5ms) (FRAME_RATE/400+FRAME_RATE/250)
	const opus_uint32 FRAMES_PER_PACKET = FRAME_RATE * 120 / 1000; // 5760

	if (argc != 4) {
		printf( "usage: opaenc <bitrate> <input.wav> <output.opa>\n"
				" note: input wav must be S16LE or F32LE sample, %u sample rate,\n"
				"       1 or 2 channels, or convert it first:\n"
				"       ffmpeg -i input.wav -c:a pcm_f32le -ac 1 -ar %u output.wav\n"
				"       ffmpeg -i input.mp3 -c:a pcm_s16le -ac 2 -ar %u output.wav\n",
			FRAME_RATE, FRAME_RATE, FRAME_RATE);
		return 1;
	}
	const opus_int32 bitrate = atoi(argv[1]); // only support <=128000
	const char* const inputPath = argv[2];
	const char* const outputPath = argv[3];
	printf("INFO:  input file: '%s'\n", inputPath);
	FILE* fp = fopen(inputPath, "rb");
	if (!fp) { printf("ERROR: can not open input file\n"); return -__LINE__; }
	opus_uint8 buf[0x24];
	if (fread(buf, 1, sizeof(buf), fp) != sizeof(buf)) { printf("ERROR: can not read head from input file\n"); return -__LINE__; }
	if (*(opus_uint32*)buf != *(const opus_uint32*)"RIFF") { printf("ERROR: input file is NOT RIFF format\n"); return -__LINE__; }
	if (*(opus_uint64*)(buf + 8) != *(const opus_uint64*)"WAVEfmt ") { printf("ERROR: input file is NOT WAVE format\n"); return -__LINE__; }
	const opus_uint32 headSize = *(opus_uint32*)(buf + 0x10);
	if (headSize < 0x10) { printf("ERROR: invalid head size of input file: %d\n", headSize); return -__LINE__; }
	const opus_uint32 format = *(opus_uint16*)(buf + 0x14); // 1:int 3:float
	if (format != 1 && format != 3) { printf("ERROR: unknown PCM format: %d\n", format); return -__LINE__; }
	const opus_uint32 channels = *(opus_uint16*)(buf + 0x16);
	if (channels != 1 && channels != 2) { printf("ERROR: input file is NOT 1 or 2 channels: %d\n", channels); return -__LINE__; }
	const opus_uint32 sampleSize = channels * (format + 1); // {2,4,8} bytes
	if (*(opus_uint32*)(buf + 0x18) != FRAME_RATE) { printf("ERROR: input file is NOT %uHz sample rate\n", FRAME_RATE); return -__LINE__; }
	if (*(opus_uint16*)(buf + 0x20) != sampleSize) { printf("ERROR: invalid sample size\n"); return -__LINE__; }
	if (*(opus_uint16*)(buf + 0x22) != (format + 1) * 8) { printf("ERROR: invalid sample bits\n"); return -__LINE__; }
	opus_uint32 tag;
	opus_uint64 size = headSize - 0x10;
	for (;;) {
		if (fseek(fp, (long)size, SEEK_CUR) != 0) { printf("ERROR: can not seek(+%llu) from input file\n", size); return -__LINE__; }
		if (fread(&tag, 4, 1, fp) != 1) { printf("ERROR: can not read tag from input file\n"); return -__LINE__; }
		if (fread(&size, 4, 1, fp) != 1) { printf("ERROR: can not read size from inpu file\n"); return -__LINE__; }
		if (tag == *(const opus_uint32*)"data") {
			if (size == 0) {
				const long cur = ftell(fp);
				if (fseek(fp, 0, SEEK_END) != 0) { printf("ERROR: can not seek end from input file\n"); return -__LINE__; }
				size = (opus_uint64)(ftell(fp) - cur);
				if (fseek(fp, cur, SEEK_SET) != 0) { printf("ERROR: can not seek back from input file\n"); return -__LINE__; }
			}
			break;
		}
	}
	if (size % sampleSize != 0) {
		printf("ERROR: input wave data is not %u-byte aligned\n", sampleSize);
		return -__LINE__;
	}
	const opus_uint64 wavFrames = size / sampleSize;
	if (wavFrames > 0xffffffffull) {
		printf("ERROR: too many wave frames: %llu\n", wavFrames);
		return -__LINE__;
	}

	int err;
	OpusEncoder* const oe = opus_encoder_create(FRAME_RATE, channels, OPUS_APPLICATION_AUDIO, &err);
	if (err != 0 || !oe) {
		printf("ERROR: opus_encoder_create failed: %d\n", err);
		return -__LINE__;
	}
	opus_encoder_ctl(oe, OPUS_SET_BITRATE(bitrate));
	opus_encoder_ctl(oe, OPUS_SET_VBR(1));
	opus_encoder_ctl(oe, OPUS_SET_VBR_CONSTRAINT(0));
	opus_encoder_ctl(oe, OPUS_SET_COMPLEXITY(10));
	opus_encoder_ctl(oe, OPUS_SET_LSB_DEPTH(16));
	opus_encoder_ctl(oe, OPUS_SET_EXPERT_FRAME_DURATION(OPUS_FRAMESIZE_120_MS));
	opus_int32 skipFrames = 0;
	opus_encoder_ctl(oe, OPUS_GET_LOOKAHEAD(&skipFrames));
	if (skipFrames != FRAMES_SKIP) {
		printf("ERROR: OPUS_GET_LOOKAHEAD=%d != %d\n", skipFrames, FRAMES_SKIP);
		return -__LINE__;
	}

	const opus_uint64 packetCount = (skipFrames + wavFrames + FRAMES_PER_PACKET - 1) / FRAMES_PER_PACKET;
	opus_uint8* const inBuf = (opus_uint8*)malloc(FRAMES_PER_PACKET * sampleSize);
	opus_uint16* const outLenBuf = (opus_uint16*)malloc(sizeof(opus_uint16) * packetCount);
	opus_int32 maxPacketSize = bitrate / 8 * 120 / 1000 * 3 / 2;
	unsigned char* const outBuf = (unsigned char*)malloc((size_t)maxPacketSize * packetCount); // ~41M/hour for 48kbps
	if (!inBuf || !outLenBuf || !outBuf) {
		printf("ERROR: malloc failed. packetCount=%llu\n", packetCount);
		return -__LINE__;
	}
	opus_uint64 outLen = 0, frames = wavFrames;
	for (opus_uint64 i = 0; i < packetCount; i++) {
		const opus_uint32 framesRead = frames < FRAMES_PER_PACKET ? (opus_uint32)frames : FRAMES_PER_PACKET;
		if (framesRead && fread(inBuf, sampleSize, framesRead, fp) != framesRead) {
			printf("ERROR: can not read wave data from input file\n");
			return -__LINE__;
		}
		if (framesRead < FRAMES_PER_PACKET)
			memset(inBuf + sampleSize * framesRead, 0, sampleSize * (FRAMES_PER_PACKET - framesRead));
		opus_int32 len;
		if (format == 1)
			len = opus_encode(oe, (opus_int16*)inBuf, FRAMES_PER_PACKET, outBuf + outLen, maxPacketSize);
		else
			len = opus_encode_float(oe, (float*)inBuf, FRAMES_PER_PACKET, outBuf + outLen, maxPacketSize);
		if (len <= 0 || len > 0x7ff/*maxPacketSize*/) { printf("ERROR: opus_encode failed: %d\n", len); return -__LINE__; }
		outLenBuf[i] = (opus_uint16)len;
		outLen += len;
		frames -= framesRead;
	}
	free(inBuf);
	opus_encoder_destroy(oe);
	fclose(fp);

	printf("INFO: output file: '%s'\n", outputPath);
	fp = fopen(outputPath, "wb");
	if (!fp) {
		printf("ERROR: can not create output file\n");
		return -__LINE__;
	}
	if (fwrite("OPA", 1, 3, fp) != 3) { printf("ERROR: can not write magic to output file\n"); return -__LINE__; }
	if (fputc('0' + channels, fp) == EOF) { printf("ERROR: can not write channels to output file\n"); return -__LINE__; }
	if (fwrite(&wavFrames, 4, 1, fp) != 1) { printf("ERROR: can not write frames to output file\n"); return -__LINE__; }
	opus_int32 last = 0;
	for (opus_uint64 i = 0; i < packetCount; i++) {
		const opus_int32 n = outLenBuf[i], d = n - last;
		if (d >= -124 && d <= 123) { // [8,255]
			if (fputc(d + 0x84, fp) == EOF) { printf("ERROR: can not write packet len(1) to output file\n"); return -__LINE__; }
		} else { // [0,7] [0,255]
			if (fputc(n >> 8  , fp) == EOF) { printf("ERROR: can not write packet len(1/2) to output file\n"); return -__LINE__; }
			if (fputc(n & 0xff, fp) == EOF) { printf("ERROR: can not write packet len(2/2) to output file\n"); return -__LINE__; }
		}
		last = n;
	}
	free(outLenBuf);
	if (fwrite(outBuf, 1, outLen, fp) != outLen) { printf("ERROR: can not write encoded data to output file\n"); return -__LINE__; }
	free(outBuf);

	const opus_uint64 totalLen = (opus_uint64)ftell(fp);
	fclose(fp);
	printf( "INFO: encode OK!\n"
			"      wav:%10llu bytes\n"
			"      opa:%10llu bytes (%llu bytes header)\n"
			"          %10llu bps\n",
		size, totalLen, totalLen - outLen, (totalLen * 200 + packetCount * 3 / 2) / (packetCount * 3));
	return 0;
}
